<?php
/**
 * Created by 784855684@qq.com.
 * Link: https://github.com/lichtung/wshore
 * User: linzh
 * Date: 6/25/17
 * Time: 4:00 PM
 */
declare(strict_types=1);


namespace wshore\core;

use wshore\core\Dao;

/**
 * Class Trace 信息跟踪
 * @package wshore\core
 */
final class Trace
{
    /**
     * @var array 跟踪信息
     */
    private static $trace = [];
    /**
     * @var array 状态信息
     */
    private static $status = [];


    /**
     * @static
     * @param array|null $keywords
     * @param array|null $trace
     * @param array|null $status
     * @return bool
     */
    public static function show(array $keywords = null, array $trace = null, array $status = null)
    {
        if (WS_IS_CLI) return false;
        $trace and self::$trace = array_merge(self::$trace, $trace);
        $status and self::$status = array_merge(self::$status, $status);

        //吞吐率  1秒/单次执行时间
        if (count(self::$status) > 1) {
            $last = end(self::$status);
            $first = reset(self::$status);            //注意先end后reset
            $stat = [
                1000 * round($last[0] - $first[0], 6),
                number_format(($last[1] - $first[1]), 6)
            ];
        } else {
            $stat = [0, 0];
        }
        $reqs = empty($stat[0]) ? 'Unknown' : 1000 * number_format(1 / $stat[0], 8) . ' req/s';

        if ($keywords) {
            $replacement = array_values($keywords);
            foreach ($replacement as &$keyword) $keyword = "<b style='background-color: yellow'>{$keyword}</b>";
        } else {
            $replacement = null;
        }
        //包含的文件数组
        $files = get_included_files();
        $info = [];
        $len = strlen(WS_PATH_BASE);
        foreach ($files as $key => $file) {
            $size = number_format(filesize($file) / 1024, 2);
            $file = substr($file, $len);
            $info[] = "{$file} Size:{$size} KB";
        }

        //运行时间与内存开销
        $fkey = null;
        $cmprst = ['Total' => "{$stat[0]}ms",];
        foreach (self::$status as $key => $val) {
            if (null === $fkey) {
                $fkey = $key;
                continue;
            }
            $cmprst["[$fkey --> $key] "] =
                number_format(1000 * floatval(self::$status[$key][0] - self::$status[$fkey][0]), 6) . 'ms&nbsp;&nbsp;' .
                number_format((floatval(self::$status[$key][1] - self::$status[$fkey][1]) / 1024), 2) . ' KB';
            $fkey = $key;
        }

        # 数据库查询
        $sqls = Dao::getLastSql(true);
        if ($len = count($sqls)) {
            $_sql = [];
            $params = Dao::getLastParams(true);
            for ($i = 0; $i < $len; $i++) {
                $_sql[$i] = '<span style="color: blue">SQL:</span> ' . $sqls[$i] .
                    ' <span style="color: blue">PARAMS:</span> ' . var_export($params[$i]??null, true);
            }
        }

        $vars = [
            'General' => [
                'request' => date('Y-m-d H:i:s', WS_NOW) . ' ' . $_SERVER['SERVER_PROTOCOL'] . ' ' . $_SERVER['REQUEST_METHOD'],
                'time' => "{$stat[0]}ms",
                'qps' => $reqs,//吞吐率
                'session id' => session_id(),
                'obcache-size' => number_format((ob_get_length() / 1024), 2) . ' KB (Unexpect Trace Page!)',//不包括trace
            ],
            'Trace' => self::$trace,
            'Files' => array_merge(['Total' => count($info)], $info),
            'Status' => $cmprst,
            'GET' => $_GET,
            'POST' => $_POST,
            'REQUEST' => $_REQUEST,
            'SERVER' => $_SERVER,
            'ENV' => $_ENV,
            'SQL' => $_sql??[],
            'COOKIE' => empty($_COOKIE) ? [] : $_COOKIE,
            'SESSION' => isset($_SESSION) ? $_SESSION : ['SESSION state disabled'],//session_start()之后$_SESSION数组才会被创建
            'IP' => [
                '$_SERVER["HTTP_X_FORWARDED_FOR"]' => isset($_SERVER['HTTP_X_FORWARDED_FOR']) ? $_SERVER['HTTP_X_FORWARDED_FOR'] : 'NULL',
                '$_SERVER["HTTP_CLIENT_IP"]' => isset($_SERVER['HTTP_CLIENT_IP']) ? $_SERVER['HTTP_CLIENT_IP'] : 'NULL',
                '$_SERVER["REMOTE_ADDR"]' => $_SERVER['REMOTE_ADDR'],
                'getenv("HTTP_X_FORWARDED_FOR")' => getenv('HTTP_X_FORWARDED_FOR'),
                'getenv("HTTP_CLIENT_IP")' => getenv('HTTP_CLIENT_IP'),
                'getenv("REMOTE_ADDR")' => getenv('REMOTE_ADDR'),
            ],
        ];

        $nav = $win = '';
        foreach ($vars as $key => $value) {
            $nav .= "<span style=\"color:#000;padding-right:12px;height:30px;line-height: 30px;display:inline-block;margin-right:3px;cursor: pointer;font-weight:700\">$key</span>";
            $win .= '<div style="display:none;"><ol style="padding: 0; margin:0">';
            if (is_array($value)) {
                foreach ($value as $k => $val) {
                    if (!is_string($val)) $val = var_export($val, true);
                    $win .= '<li style="border-bottom:1px solid #EEE;font-size:14px;padding:0 12px"><span style="color: blue">' .
                        (is_numeric($k) ? '' : $k . ':</span>') .
                        "<span  style='color:black'>{$val}</span></li>";
                }
            } else {
                $win .= htmlspecialchars($value, ENT_COMPAT, 'utf-8');
            }
            $win .= '</ol></div>';
        }
        //高亮显示关键字
        $replacement and $file = str_replace($keywords, $replacement, $win);
        echo <<< endline
    <div style="border-left:thin double #ccc;position: fixed;bottom:0;right:0;font-size:14px;width:1280px;z-index: 999999;color: #000;text-align:left;font-family:'Times New Roman';cursor:default;">
        <div id="ptt" style="display: none;background:white;margin:0;height: 512px;">
            <!-- 导航条 -->
            <div id="pttt" style="height:32px;padding: 6px 12px 0;border-bottom:1px solid #ececec;border-top:1px solid #ececec;font-size:16px">
                {$nav}
            </div>
            <!-- 详细窗口 -->
            <div id="pttc" style="overflow:auto;height:478px;padding: 0; line-height: 24px">
                {$win}
            </div>
        </div>
        <!-- 关闭按钮 -->
        <div id="ptc" style="display:none;text-align:right;height:15px;position:absolute;top:10px;right:12px;cursor: pointer;">
            <img style="vertical-align:top;" src="" />
        </div> 
    </div>
    <!-- 开启按钮 -->
    <div id="pto" style="height:30px;float:right;text-align: right;overflow:hidden;position:fixed;bottom:0;right:0;color:#000;line-height:30px;cursor:pointer;">
        <div style="background:#232323;color:#FFF;padding:0 6px;float:right;line-height:30px;font-size:14px"></div>
    <img style="width: 30px" title="ShowPageTrace" src="" />
    </div>
    <script type="text/javascript">
    (function(){
        var tab_tit  = document.getElementById('pttt').getElementsByTagName('span');
        var tab_cont = document.getElementById('pttc').getElementsByTagName('div');
        var open     = document.getElementById('pto');
        var close    = document.getElementById('ptc');
        var trace    = document.getElementById('ptt');
        var cookie   = document.cookie.match(/_spt=(\d\|\d)/);
        var history  = (cookie && typeof cookie[1] != 'undefined' && cookie[1].split('|')) || [0,0];
        open.onclick = function(){
            trace.style.display = '';
            close.style.display = '';
            history[0] = 1;
            document.cookie = '_spt='+history.join('|');
        };
        close.onclick = function(){
            trace.style.display = 'none';
            open.style.display = 'block';
            history[0] = 0;
            document.cookie = '_spt='+history.join('|');
        };
        for(var i = 0; i < tab_tit.length; i++){
            tab_tit[i].onclick = (function(i){
                return function(){
                    for(var j = 0; j < tab_cont.length; j++){
                        tab_cont[j].style.display = 'none';
                        tab_tit[j].style.color = '#999';
                    }
                    tab_cont[i].style.display = 'block';
                    tab_tit[i].style.color = '#000';
                    history[1] = i;
                    document.cookie = '_spt='+history.join('|')
                }
            })(i);
        }
        parseInt(history[0]) && open.click();
        (tab_tit[history[1]] || tab_tit[0]).click();
    })();
    </script>
endline;
        return true;
    }

    /**
     * 显示和记录错误信息
     * @param string $message 错误信息内容
     * @param string $classnm 错误名称
     * @param string $file 错误发生的文件
     * @param int $line 错误发生的行
     * @param array $traces 错误回溯
     * @return void
     */
    public static function handleTrowable(string $message, string $classnm, string $file, int $line, array $traces)
    {
        Response::cleanOutput();
        $infos = [
            'message' => $message,
            'file' => $file,
            'line' => $line,
//            'trace' => $traces,
            'class' => $classnm,
        ];

        # 记录错误日志
        Logger::getLogger('throwable')->fatal($infos);

        if (WS_IS_CLI) {
            var_dump($infos);
        } elseif (WS_IS_AJAX) {
            Response::export([
                'status' => -1,
                'message' => WS_DEBUG_MODE_ON ? var_export($infos, true) : 'an error occur',
            ]);
        } else {
            if (WS_DEBUG_MODE_ON) {
                // Display error message
                echo '<style>
				#lite_throwable_display{ font-size: 14px;font-family: "Consolas", "Bitstream Vera Sans Mono", "Courier New", Courier, monospace}
				#lite_throwable_display h2{color: #F20000}
				#lite_throwable_display p{padding-left: 20px}
				#lite_throwable_display ul li{margin-bottom: 10px}
				#lite_throwable_display a{font-size: 12px; color: #000000}
				#lite_throwable_display .psTrace, #lite_throwable_display .psArgs{display: none}
				#lite_throwable_display pre{border: 1px solid #236B04; background-color: #EAFEE1; padding: 5px;  width: 99%; overflow-x: auto; margin-bottom: 30px;}
				#lite_throwable_display .psArgs pre{background-color: #F1FDFE;}
				#lite_throwable_display pre .selected{color: #F20000; font-weight: bold;}
			</style>';
                echo '<div id="lite_throwable_display">';
                echo '<h2>[' . $classnm . ']</h2>';
                echo self::_buildContent($message, $file, $line);

                echo self::_buildFileBlock($file, $line);

                // Display debug backtrace
                echo '<ul>' . self::_buildTrace($traces) . '</ul></div>';
            } else {
                \wshore::template('404');
            }
        }
        die;
    }

    /**
     * @param array $traces
     * @return string
     */
    private static function _buildTrace(array $traces): string
    {
        $html = '';
        foreach ($traces as $id => $trace) {
            # Error和Exception获取的trace是不一样的，Exception包含自身提示信息的部分，但是Error少了这一部分，所以把下面的代码注释
//            if(!$id) continue;
            $relative_file = (isset($trace['file'])) ? ltrim(str_replace(array(WS_PATH_BASE, '\\'), array('', '/'), $trace['file']), '/') : '';
            $current_line = (isset($trace['line'])) ? $trace['line'] : '';
            $html .= '<li>';
            $html .= '<b>' . ((isset($trace['class'])) ? $trace['class'] : '') . ((isset($trace['type'])) ? $trace['type'] : '') . $trace['function'] . '</b>';
            $html .= ' - <a style="font-size: 12px;  cursor:pointer; color: blue;" onclick="document.getElementById(\'psTrace_' . $id . '\').style.display = (document.getElementById(\'psTrace_' . $id . '\').style.display != \'block\') ? \'block\' : \'none\'; return false">[line ' . $current_line . ' - ' . $relative_file . ']</a>';

            if (isset($trace['args']) && count($trace['args'])) {
                $html .= ' - <a style="font-size: 12px;  cursor:pointer; color: blue;" onclick="document.getElementById(\'psArgs_' . $id . '\').style.display = (document.getElementById(\'psArgs_' . $id . '\').style.display != \'block\') ? \'block\' : \'none\'; return false">[' . count($trace['args']) . ' Arguments]</a>';
            }

            if ($relative_file) {
                $html .= self::_buildFileBlock($trace['file'], $trace['line'], $id);
            }
            if (isset($trace['args']) && count($trace['args'])) {
                $html .= self::_buildArgsBlock($trace['args'], $id);
            }
            $html .= '</li>';
        }
        return $html;
    }


    /**
     * Display lines around current line
     *
     * @param string $file
     * @param int $line
     * @param string $id
     * @return string
     */
    private static function _buildFileBlock($file, $line, $id = null): string
    {
        $lines = file($file);
        $offset = $line - 6;
        $total = 11;
        if ($offset < 0) {
            $total += $offset;
            $offset = 0;
        }
        $lines = array_slice($lines, $offset, $total);
        ++$offset;

        $html = '<div class="psTrace" id="psTrace_' . $id . '" ' . ((is_null($id) ? 'style="display: block"' : '')) . '><pre>';
        foreach ($lines as $k => $l) {
            $string = ($offset + $k) . '. ' . htmlspecialchars($l);
            if ($offset + $k == $line) {
                $html .= '<span class="selected">' . $string . '</span>';
            } else {
                $html .= $string;
            }
        }
        return $html . '</pre></div>';
    }


    /**
     * Display arguments list of traced function
     *
     * @param array $args List of arguments
     * @param string $id ID of argument
     * @return string
     */
    private static function _buildArgsBlock($args, $id): string
    {
        $html = '<div class="psArgs" id="psArgs_' . $id . '"><pre>';
        foreach ($args as $arg => $value) {
            $html .= '<b>args [' . Security::strip((string)$arg) . "]</b>\n";
            $html .= Security::strip(print_r($value, true));
            $html .= "\n";
        }
        return $html . '</pre>';
    }

    /**
     * Return the content of the Exception
     * @param string $message
     * @param string $file
     * @param int $line
     * @return string content of the exception
     */
    private static function _buildContent(string $message, string $file, int $line): string
    {
        $format = '<p><b style="color: mediumslateblue">%s</b><br/><i>at line </i><b>%d</b><i> in file </i><b style="color: darkorchid">%s</b></p>';
        return sprintf($format, $message, $line, ltrim(str_replace(array(WS_PATH_BASE, '\\'), array('', '/'), $file), '/'));
    }

}